/* TableDataSource.m
 *
 *  This data source reads a flat file table (such as Product.table or Item.table) 
 *  and generates EOGeneric records that can be passed to an EOController.
 *
 * You may freely copy, distribute, and reuse the code in this example.
 * NeXT disclaims any warranty of any kind, expressed or  implied, as to its
 * fitness for any particular use.
 *
 *
 *
 */

#import <appkit/appkit.h>
#import <eoaccess/eoaccess.h>
#import "TableDataSource.h"
#import "TableDataSourcePrivate.h"
#import "DetailTableDataSource.h"
#include <sys/file.h> 


#define @QUALIFIER_ALL @"*"
#define @QPROPERTY @"PROPERTY"
#define @QVALUE @"VALUE"

@implementation TableDataSource

// init a new TableDataSource with an array of EOGeneric records and the path to
// the table file (the flat file database!). The entity name will be found from a
// record in the array.
// Use this initialization method when you have existing records (possibly from
// a real database) and you want to save them to a file
- initWithEOGenericRecords:(NSArray *)records tablePath:(NSString *)tablePath {
	NSString *tableFile;
	NSMutableArray *dictArray = [NSMutableArray array];
	NSDictionary *dict;
	int i;

	// We need at least a record to get the entity
	if (![records count]) {
		NSLog(@"<initWithEOGenericRecords:tablePath:> No records");
		return nil;
	}
	entity = [[records objectAtIndex:0] entity];
	tableFile=[NSString stringWithFormat:@"%@/%@.table", tablePath, [entity name]];
	for (i=0; i<[records count]; i++) {
		dict = [[records objectAtIndex:i] valuesForKeys:[entity classPropertyNames]];
		[dictArray addObject:dict];
	}
	
	// Save the records to a file as a property-list string
	if (![[[dictArray description] dataUsingEncoding:NSASCIIStringEncoding] 
			writeToFile:tableFile atomically:NO]) {
		NSLog(@"<initWithEOGenericRecords:tablePath:> DataSource %@ failed to save", [entity name]);
		return nil;
	}
	// Call the designated initialization method
	else return [self initWithEntity:entity tablePath:tablePath];
	
}

// The designated initialization method for TableDataSource
// Pick an entity in the model, then initialize the source from a flat file that
// contains a string representation of an array of dictionaries.
// Note that unlike the EODatabaseSataSource, this dataSource has a "state".
// It cashes the eos (dictionaries) that it read from the file and also maintains 
// a few hash tables to speed up fetch operations...
// Although the state of the TableDataSource is an array of dictionaries (I call it
// the snapshot) the TabledataSource will copy these dictionaries into an array of 
// EOGeneric records before handing them to a controller...
- initWithEntity:(EOEntity *)anEntity tablePath:(NSString *)tablePath {
	NSString *primaryKey;
	NSArray *relations;
	NSMutableArray *qualifiers = [NSMutableArray array];

	[super init];
	entity = [anEntity retain];
	primaryKey = [(EOAttribute *)[[entity primaryKeyAttributes] objectAtIndex:0] name];
	relations = [entity relationships];
	detailSources = [[NSMutableArray array] retain]; 
	return [self initWithTable:tablePath primaryKey:primaryKey qualifierKeys:qualifiers];
}

// The new NSObject world, I guess
- (void)dealloc {
	[table autorelease];
	[eos autorelease];
	[lookupTables autorelease];
	[qualifier autorelease];
	[uniqueKey autorelease];
	[detailSources autorelease];
	[entity autorelease];
	[orderByKey autorelease];
	[super dealloc];
}

// The path to persistent storage, here to the "table" file... 
- (NSString *)tablePath {
	return table;
}

// The other one...
- setTablePath:(NSString *)aPath {
	[table autorelease];
	table = [aPath retain];
	return self;
}

// The supported entity, we got it from the EOmodel at initialization time
- (EOEntity *)entity {
	return entity;
}

// A custom way (meaning different from the EOF way) to qualify the source for a given property name and value
// The equivalent SQL is: WHERE( key = value)
- qualifyForProperty:(NSString *)key andValue:value {
	NSMutableDictionary *qual = [NSMutableDictionary dictionary];
	
	if (![[self keys] containsObject:key]) {
		NSLog(@"%@ is not a valid property of entity %@", key, [entity name]);
		qual=nil;
	}
	else {
		[qual setObject:key forKey:@QPROPERTY];
		[qual setObject:value forKey:@QVALUE];
	}
	return [self setQualifier:qual];
}

// Qualify for all records
- setEntityQualifier {
	NSMutableDictionary *qual =[NSMutableDictionary dictionary];
	[qual setObject:uniqueKey forKey:@QPROPERTY];
	[qual setObject:@QUALIFIER_ALL forKey:@QVALUE];
	return [self setQualifier:qual];
}

// Set the qualifier before the fetch
- setQualifier:(NSMutableDictionary *)newQualifier {
	[qualifier autorelease];
	if (newQualifier)  qualifier  = [newQualifier retain];
	else qualifier=nil;
	//NSLog(@"New qualifier %@", [(NSMutableDictionary *)qualifier description]);
	return self;
}

- setEmptySetQualifier {
	[qualifier autorelease];
	qualifier=nil;
	return self;
}

- (BOOL)orderBy:(NSString *)key {
	if (![[self keys] containsObject:key]) return NO;
	else {
		[orderByKey autorelease];
		orderByKey = [key retain];
		[eos sortUsingFunction:eoSort context:orderByKey];
		return YES;
	}
}

- setOrderDescendantSources:(BOOL)aFlag {
	orderDescendantSources=aFlag;
	return self;
}

// *************************************  DATASOURCE PROTOCOL ****************/
// The class property names (keys for the dictionary or EOGenericRecord)
- (NSArray *)keys {
	NSArray *attrs = [entity attributes];
	NSMutableArray *keys = [NSMutableArray array];
	int i;

	for (i=0; i<[attrs count]; i++) {
		EOAttribute *at = [attrs objectAtIndex:i];
		[keys addObject:[at name]];
	}
	return keys;
}

- createObject{
	NSNumber *newId=[self findNextPrimaryKey];
	NSMutableDictionary *newObject=[[NSMutableDictionary alloc] init];
	
	if (!newId) return nil;
	else {
		NSArray *allKeys = [self keys];
		NSArray *relations = [entity relationships];
		int i, count;

		for (i=0, count=[allKeys count]; i<count; i++) {
			id key = [allKeys objectAtIndex:i];
			[newObject setObject:@"" forKey:key];
		}
		for (i=0, count=[relations count]; i<count; i++) {
			EORelationship *rel = [relations objectAtIndex:i];
			if ([rel isToMany]) [newObject setObject:[newId description] forKey:[rel name]];
		}
		[newObject setObject:[newId description] forKey:uniqueKey];
		return newObject;
	}
}

- (BOOL)insertObject:object {
	NSNumber *objectId=[object objectForKey:uniqueKey];
	NSArray *allKeys;
	int i;
	NSMutableDictionary *newObject;

	if (![self isValidNewPrimaryKey:objectId]) {
		NSLog(@"DataSource cannot insert; primary key is invalid: %@", [(NSString *)objectId description]);
		return NO;
	}

	newObject=[NSMutableDictionary dictionary];
	allKeys = [self keys];
	for (i=0; i<[allKeys count]; i++) {
		id key = [allKeys objectAtIndex:i];
		id value = [object objectForKey:key];
		if ([value isEqual:[EONull null]]) value=@"";
		[newObject setObject:value forKey:key];
	}
	
	[eos addObject:newObject];
	if (orderByKey) [eos sortUsingFunction:eoSort context:orderByKey];
	[self modifyLookupTables];
	return YES;
}

- (BOOL)deleteAllObjects {
	int i;
	for (i=[eos count]-1; i>=0; i--) {
		[eos removeObject:[eos objectAtIndex:i]];
	}
	[self modifyLookupTables];
	return YES;
}

- (BOOL)deleteObject:object {
	id primaryKeyValue = [object objectForKey:uniqueKey];
	NSMutableArray *deletes=[[lookupTables objectForKey:uniqueKey] objectForKey:primaryKeyValue];
	int i;

	if ( !deletes || ![deletes count]) return NO;
	for (i=0; i<[deletes count]; i++) {
		[eos removeObject:[deletes objectAtIndex:i]];
	}
	[self modifyLookupTables];
	return YES;
}

- (BOOL)updateObject:object {
	id primaryKeyValue = [object objectForKey:uniqueKey];
	NSMutableArray *update=[[lookupTables objectForKey:uniqueKey] objectForKey:primaryKeyValue];
	NSArray *allKeys;
	NSMutableDictionary *eo;
	int i;
	NSString *key;
	id value;

	if (!update || [update count]!=1) return NO;
	allKeys = [self keys];
	eo = [update objectAtIndex:0];
	for (i=0; i<[allKeys count]; i++) {
		key = [allKeys objectAtIndex:i];
		value = [object objectForKey:key];
		if (!value || [value isEqual:[EONull null]] ) value=@"";
		[eo setObject:value forKey:key];
	}
	if (orderByKey) [eos sortUsingFunction:eoSort context:orderByKey];
	[self modifyLookupTables];
	return YES;
}

- (NSArray *)fetchObjects {
	NSArray *records;
	NSString *qualifierPropertyKey = [qualifier objectForKey:@QPROPERTY];
	NSString *qualifierPropertyValue = [qualifier objectForKey:@QVALUE];

	if (qualifierPropertyKey && qualifierPropertyValue) {

		if ( [qualifierPropertyValue isEqual:@QUALIFIER_ALL] ) {
			records =[self eosArrayToGenericRecordsArray:eos];
		}
		else {
			NSMutableDictionary *hash = [lookupTables objectForKey:qualifierPropertyKey];
			if (!hash) {
				NSLog(@"No lookup table for qualifier key %@", qualifierPropertyKey);
				records =nil;
			}
			else {
				NSArray *qualifiedEos=[hash objectForKey:qualifierPropertyValue];
				if (!qualifiedEos || ![qualifiedEos count]) {
					//NSLog(@"No qualified eos where %@=%@", qualifierPropertyKey, [(NSString *)qualifierPropertyValue description]);
					records =nil;
				}
				records =[self eosArrayToGenericRecordsArray:qualifiedEos];
			}

		}
	}
	else {
		//NSLog(@"nil or uncomplete qualifier %@", [(NSMutableDictionary *)qualifier description]);
		records = [NSArray array]; 
	}
	//NSLog(@"TABLE %@ - FETCHED OBJECTS: %d", [entity name], [records count]);
	return records;
}

// This is where we save the eos to persistent storage (COMMIT in SQL)
- (BOOL)saveObjects{
	NSString *tableFile=[NSString stringWithFormat:@"%@/%@.table", table, [entity name]];
	if (![[[eos description] dataUsingEncoding:NSASCIIStringEncoding] 
			writeToFile:tableFile atomically:NO]) {
		NSString *emess;
 
		if ( (!access([tableFile cString], F_OK)) && (access([tableFile cString], W_OK)) ) {
			int ans;
			emess = [NSString stringWithFormat:@"\'%@\' data source cannot save changes. You do not have write permission on \'%@\'. Do you want to overwrite?", [entity name], tableFile];
			ans=NXRunAlertPanel("ERROR", [emess cString], "Overwrite", "Cancel", NULL);
			if (ans==NX_ALERTDEFAULT) return [self forceSaveObjects];
			else return NO;
		}
		else return NO;
	}
	else return YES;
}

- (BOOL)canDelete {
	return YES;
}

// Not implemented yet...
// Coerce a value to the appropriate type.
// This method should convert to either an NSNumber, NSString, NSData,
// a custom type, or nil.  The value return by this method may be safely
// passed to an EO via takeValuesFromDictionary:.  This method is used
// by controllers to coerce values supplied from associations before
// those values are passed on to the EOs.
- coerceValue: value forKey: (NSString *)key {
	// sorry, another day , maybe...
	return value;
}

// ******************************  MASTER DATASOURCE PROTOCOL ****************/
// What we are doing here is closer to a master-peer than a master-detail setup.
// We are creating the detail dataSource and handing it to the association. 
// This new detail dataSource will be attached to the detail controller automatically
// From there, the detail controller and its DetailTableDataSource will be on their own
// Also notice than in an EOF master-detail setup, a master eo (meaning an eo 
// produced by the master source) "carries" its detail eos. In other word if one 
// writes [masterEo objectForKey:masterDetailRelationshipKey], the master eo will 
// return an array full of detail eos (assuming that the master detail relationship
// was set as a class property name in EOModeler). Although it could, the TableDataSource 
// does not support that feature...
- (id <EOQualifiableDataSources>)dataSourceQualifiedByKey:(NSString *)key {
	EORelationship *rel = [entity relationshipNamed:key];
	EOEntity *detailEntity=[rel destinationEntity];
	DetailTableDataSource *detailSource;

	if (!rel) return nil;
	if (!detailEntity) return nil;
	detailSource = [[(DetailTableDataSource *)[DetailTableDataSource alloc] initWithMasterDataSource:self entity:detailEntity] autorelease];	
	if (orderByKey && orderDescendantSources) [detailSource orderBy:orderByKey];

	// We cash it (I had some grandiose plane, unused today!!)
	[detailSources addObject:detailSource];
	return detailSource;
}

// ***************************************************************************/

// a convenience method to fetch all the objects in the entity
- (NSArray *)fetchAllObjects {
	NSMutableDictionary *qual =[NSMutableDictionary dictionary];
	[qual setObject:uniqueKey forKey:@QPROPERTY];
	[qual setObject:@QUALIFIER_ALL forKey:@QVALUE];
	[self setQualifier:qual];
	return [self fetchObjects];
}

// a convenience fetch method
- objectForPrimaryKey:value {
	id hash = [lookupTables objectForKey:uniqueKey];
	NSArray *objects = [hash objectForKey:value];
	id object;
	EOGenericRecord *record;
	NSMutableDictionary *primaryKeyDictionary;
	NSString *key;
	id copy;
	int count, j;
	NSArray *allKeys = [self keys];

	if ([objects count]!=1) return nil;
	object = [objects objectAtIndex:0];
	primaryKeyDictionary=[NSMutableDictionary dictionary];
	[primaryKeyDictionary setObject:value forKey:uniqueKey];
	record = [[EOGenericRecord alloc] initWithPrimaryKey:primaryKeyDictionary 		
										entity:entity];
	for (j=0, count=[allKeys count]; j<count; j++) {
		key = [allKeys objectAtIndex:j];
		copy = [[object objectForKey:key] copy];
		[record setObject:[copy autorelease] forKey:key];
	}
	return [record autorelease];
}

- addLookupTableForKey:(NSString *)key {
	NSMutableDictionary *hash;

	hash = [self createLookupTableForQualifierKey:key];
	[lookupTables setObject:hash forKey:key];
	return self;
}



@end

// *************************************  Private Category ****************/
@implementation TableDataSource (Private)

// This initialization method is called by initWithEntity:tablePath:
// It sets the key for the primary key  and creates a key->array of objects hash
// tables for each element of the qualifiers array.
- initWithTable:(NSString *)tablePath primaryKey:(NSString *)primaryKey qualifierKeys:(NSArray *)qualifiers {

	// Create snapshot from table (eos)
	table = [tablePath retain];
	if (![self createSnapshot]) return nil;

	// Create a dictionary of hash tables for query (key is like @"ID", value is a lookup table)
	uniqueKey = [primaryKey retain];
	if ( !([self createLookupTables:qualifiers]) ) return nil;

	// set qualifier to select everything 
	// To be clean, we should have a qualifier Class, oh well...it is a rush job
	// after all
	orderByKey=nil;
	orderDescendantSources = NO;
	qualifier = [[NSMutableDictionary alloc] init];
	[qualifier setObject:uniqueKey forKey:@QPROPERTY];
	[qualifier setObject:@QUALIFIER_ALL forKey:@QVALUE];

	return self;
}

// The flat file is read here and the objects that have been retrieved are cashed
// in an instance variable (of class NSMutableArray) called eos
- createSnapshot {
	NSString *tableFile=[NSString stringWithFormat:@"%@/%@", table, [entity externalName]];
	NSString *tableString;
	NSArray  *nonMutableEos;

	// read the table file that should be in a property list format	
	// Isn't foundation great or what?!!!
	if ( !(tableString=[[NSString alloc] initWithContentsOfFile:tableFile]) ||
		 !(nonMutableEos = [tableString propertyList]) ) {
			NSLog(@"<createSnapshot> Cannot init from table %@", tableFile);
			return nil;
	}
	// Make deep mutable copy
	else {
		NSEnumerator *eoEnumerator = [nonMutableEos objectEnumerator];
		NSDictionary *nonMutableEo;

		eos = [[NSMutableArray alloc] init];
		while ( nonMutableEo = [eoEnumerator nextObject] ) {
			[eos addObject: [[nonMutableEo mutableCopy] autorelease]];
		}
	}
	return self;
}

// Create the lookup tables (to speed up fetch operations)
// The qualifiers array contains the property names that we should hash on
// a lookup table is in the form key = "a property key" --> value = an array of 
// dictionaries...
- createLookupTables:(NSArray *)qualifiers {
	int i, count;
	NSMutableDictionary *hash;
	NSString *qualifierKey;

	lookupTables = [[NSMutableDictionary alloc] init];
	hash = [self createLookupTableForQualifierKey:uniqueKey];
	[lookupTables setObject:hash forKey:uniqueKey];

	for (i=0, count=[qualifiers count]; i<count; i++) {
	
		qualifierKey = [qualifiers objectAtIndex:i];
		if ( ![lookupTables objectForKey:qualifierKey] ) {
			//NSLog(@"Creating lookup table for qualifierKey %@", qualifierKey);
			if ( !(hash = [self createLookupTableForQualifierKey:qualifierKey]) ) return nil;
			[lookupTables setObject:hash forKey:qualifierKey];
		}
	}
	return self;
}

// Create a lookup table for a given property name (the key)
- createLookupTableForQualifierKey:(NSString *)key {

	NSMutableDictionary *hash;
	int i, count;
	NSDictionary *eo;
	NSMutableArray *values;
	id value;
	
 	hash=[NSMutableDictionary dictionary];

	for (i=0, count=[eos count]; i<count; i++) {

		eo = [eos objectAtIndex:i];

		// get the property value for the qualifier key
		if ( !(value = [eo objectForKey:key]) ) {
			NSLog(@"<createLookupTableForQualifierKey:> eos dictionary does not respond to qualifier key %@", key);
			return nil;
		}
	
		// It is the first time that we encounter an eo such that propertyValue(key) = value
		if ( !(values = [hash objectForKey:value]) ) {
			values = [NSMutableArray array];
			[values addObject:eo];
			[hash setObject:values forKey:[(NSObject *)value description]];
		}
		// We have already found an eo such that propertyValue(key) = value
		else {
			[values addObject:eo];
		}
	}
	//NSLog(@"HASH FOR KEY %@\n%@", key, [(NSDictionary *)hash description]);
	return hash;
}

// After insert, delete or update opeations, the lookup tables must be refreshed
- modifyLookupTables {
	NSArray *allKeys=[lookupTables allKeys];
	int i, count;
	NSString *key;
	NSMutableDictionary *hash;

	for (i=0, count=[allKeys count]; i<count; i++) {
		key = [allKeys objectAtIndex:i];
		[lookupTables removeObjectForKey:key];
		hash = [self createLookupTableForQualifierKey:key];
		[lookupTables setObject:hash forKey:key];
	}
	return self;
}

// a method to transform an array of eos (state of the TableDataSource) into
// an array of EOGeneric records that we can hand to the outside worls...
- (NSMutableArray *)eosArrayToGenericRecordsArray:(NSArray *)eoArray {
	NSMutableArray *records = [NSMutableArray  array];
	int i, eoCount;

	for (i=0, eoCount=[eoArray count]; i<eoCount; i++) {
		NSMutableDictionary *eo;
		NSMutableDictionary *primaryKey=[NSMutableDictionary dictionary];
		EOGenericRecord *record;
		NSArray *allKeys;
		NSArray *relations;
		int j, count;

		eo = [eoArray objectAtIndex:i];
		allKeys = [self keys];
		[primaryKey setObject:[eo objectForKey:uniqueKey] forKey:uniqueKey];
		record = [[[EOGenericRecord alloc] initWithPrimaryKey:primaryKey entity:entity] autorelease];
		for (j=0, count=[allKeys count]; j<count; j++) {
			NSString *key = [allKeys objectAtIndex:j];
			id copy;

			copy = [[eo objectForKey:key] copy];
			[record setObject:[copy autorelease] forKey:key];
		}
		relations = [entity relationships];

		// We support master-details but not flattened attribute for now!!!
		for (j=0, count=[relations count]; j<count; j++) {
			EORelationship *rel = [relations objectAtIndex:j];
			id copy;
	
			if ([rel isToMany]) {
				copy = [[eo objectForKey:uniqueKey] copy];
				[record setObject:[copy autorelease] forKey:[rel name]];
			}

		}
		[records addObject:record];
	}
	return records;
}

// Unlike the EODatabaseDataSource, when a TableDataSource creates a new object
// It sets the primary key value for the created object. 
// This method computes the next available primary key (the next in the sequence)
- (NSNumber *)findNextPrimaryKey {
	int i, max=-1;

	for (i=0; i<[eos count]; i++) {
		id eo;
		int current;

		eo = [eos objectAtIndex:i];
		current = [[eo objectForKey:uniqueKey] intValue];
		if (current  > max) max=current;
	}
	return [NSNumber numberWithInt:max+1];
}

// make sure the the primary key of a new object handed for insertion has a valid primary key
// (in this case, valid means not used by a record stored in the table)
- (BOOL)isValidNewPrimaryKey:(NSNumber *)number {
	int i;

	for (i=0; i<[eos count]; i++) {
		id eo;

		eo = [eos objectAtIndex:i];
		if ([number isEqual:[eo objectForKey:uniqueKey]]) return NO;
	}
	return YES;
}

- (BOOL)forceSaveObjects {
	NSString *tableFile=[NSString stringWithFormat:@"%@/%@.table", table, [entity name]];
	NSString *emess=@"Cannot overwrite; changes will not be saved...";
	NSString *cmd;

	cmd = [NSString stringWithFormat:@"mv %@ %@.otto", tableFile, tableFile];
	if (system([cmd cString])) {
		NXRunAlertPanel("ERROR", [emess cString], "OK", NULL, NULL);
		return NO;
	}
	cmd = [NSString stringWithFormat:@"cp %@.otto %@; ", tableFile, tableFile];
	if (system([cmd cString])) {
		NXRunAlertPanel("ERROR", [emess cString], "OK", NULL, NULL);
		return NO;
	}
	cmd = [NSString stringWithFormat:@"rm -f %@.otto; chmod u+w %@", tableFile, tableFile];
	system([cmd cString]);

	if (![[[eos description] dataUsingEncoding:NSASCIIStringEncoding] 
			writeToFile:tableFile atomically:NO]) return NO;
	else return YES;
	
}

int eoSort(id eo1, id eo2, void *context) {
	NSString *key=(NSString *)context;
	NSString *val1, *val2;

	if (!key) return NSOrderedSame;
	val1 = [eo1 objectForKey:key];
	val2 = [eo2 objectForKey:key];
	if (!val1 || !val2) return NSOrderedSame;
	else return [val1 compare:val2];
}


@end
